/*

This file is part of Rooms3D.

Copyright (C) 2008 Pixecur Technologies Incorporated (Esotera).
Visit Esotera at http://www.esoteras3.com
 
Rooms3D is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
 
Rooms3D is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with Rooms3D; see the file COPYING.  If not, write to 
Pixecur Technologies, 213-31 Peet Street, St. John's, NL, A1B 3W8. 
 
Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.
 
*/

package com.datasentinel.rooms.ui.sh3d;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.Material;
import javax.media.j3d.Node;
import javax.media.j3d.PickInfo;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.RenderingAttributes;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Texture;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransparencyAttributes;
import javax.media.j3d.VirtualUniverse;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.event.EventListenerList;
import javax.swing.event.MouseInputAdapter;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;

import com.datasentinel.file.FileBuilder;
import com.datasentinel.file.FileBuilderFactory;
import com.datasentinel.j3d.controls.hud.ClipboardContentsViewer;
import com.datasentinel.j3d.controls.hud.HeadsUpDisplay;
import com.datasentinel.rooms.banner.sh3d.BannerPieceFurnitureSupport;
import com.datasentinel.rooms.banner.sh3d.BannerPieceOfFurniture;
import com.datasentinel.rooms.banner.sh3d.BannerPieceOfFurnitureBuilder;
import com.datasentinel.rooms.banner.sh3d.BannerStringPieceOfFurniture;
import com.datasentinel.rooms.file.FolderChangeListener;
import com.datasentinel.rooms.file.FolderMonitor;
import com.datasentinel.rooms.file.sh3d.FilePieceOfFurniture;
import com.datasentinel.rooms.file.sh3d.FilePieceOfFurnitureBuilder;
import com.datasentinel.rooms.file.sh3d.FilePieceOfFurnitureTextureManager;
import com.datasentinel.rooms.file.sh3d.HaiVanPassPictureFrameFurnitureSupport;
import com.datasentinel.rooms.file.sh3d.HomeFilePieceOfFurniture;
import com.datasentinel.rooms.file.sh3d.HomeFilePieceOfFurnitureArrangeableSupport;
import com.datasentinel.rooms.j3d.util.FileThumbnailTextureLoader;
import com.datasentinel.rooms.j3d.util.ImagePlaneShape3D;
import com.datasentinel.rooms.j3d.util.PlaneShape3D;
import com.datasentinel.rooms.j3d.util.Texturable;
import com.datasentinel.rooms.layout.Random2DArrangement;
import com.datasentinel.rooms.swing.util.TimedMouseEventHandler;
import com.datasentinel.rooms.swing.util.TimedMouseEventHandlerCallback;
import com.datasentinel.rooms.util.sh3d.CollisionUtils;
import com.datasentinel.rooms.util.sh3d.FurnitureUtils;
import com.datasentinel.util.file.FilenameGeneratorForCopies;
import com.datasentinel.util.thumbnail.ThumbnailException;
import com.datasentinel.util.ui.gesture.ActivationGestureCallback;
import com.datasentinel.util.ui.gesture.ActivationGestureEvent;
import com.datasentinel.util.ui.gesture.GestureEvent;
import com.datasentinel.util.ui.gesture.GestureListener;
import com.datasentinel.util.ui.gesture.ImportGestureEvent;
import com.datasentinel.util.ui.gesture.ImportGestureEventCallback;
import com.datasentinel.util.ui.gesture.StatusCallback;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.model.Wall;
import com.eteks.sweethome3d.swing.HomeComponent3D;
import com.eteks.sweethome3d.swing.HomeController3D;
import com.sun.j3d.utils.geometry.Box;
import com.sun.j3d.utils.pickfast.PickCanvas;
import com.sun.j3d.utils.universe.PlatformGeometry;

public class CustomHomeComponent3D extends HomeComponent3D
{
	private static final Color LIGHT_BLUE = new Color(0xAD, 0xD8, 0xE6);
	private static final Color LIGHT_PINK = new Color(0xE6, 0xB6, 0xAD);
	private static final long serialVersionUID = -3565758443399036259L;		
	public static final Object CR = System.getProperty("line.separator");

	public enum ActionType {
		COPY_OPERATION, PASTE_OPERATION, UNDO_OPERATION, ARROW_LEFT, ARROW_RIGHT, CUT_OPERATION, 
		DELETE_OPERATION, SELECTALL_OPERATION, NEW_BANNER
	}

	private PickCanvas roomPicker;
	private PickCanvas vpPicker;
	private FileThumbnailTextureLoader fileThumbnailTextureLoader;
	private EventListenerList gestureEventListenerList = new EventListenerList();
	private static Logger logger = Logger.getLogger(CustomHomeComponent3D.class.getName());
	private static TransparencyAttributes transparencyAttributes;
	private final Object fileThumbnailTextureLoaderLock = new Object();
	private static Executor executor = Executors.newSingleThreadExecutor();
	private MouseInputAdapter mouseInputAdapter;
	private FolderChangeListener folderChangeListener;
	private Map<File, HomeFilePieceOfFurniture> fileFurniturePieceMap;
	private Map<StringBuffer, Texturable> bannerFurniturePieceMap;
	private List<StatusCallback> callbacks = Collections.synchronizedList(new ArrayList<StatusCallback>());
	private HeadsUpDisplay headsUpDisplay; 
	private ClipboardContentsViewer clipboardContentsViewer;
	private static FileBuilder fileBuilder = FileBuilderFactory.createFileBuilder();
	
	private AtomicBoolean transferCancelled = new AtomicBoolean();
	private AtomicReference<Object> currentTransferTarget = new AtomicReference<Object>();
	static
	{
		transparencyAttributes = new TransparencyAttributes();
		transparencyAttributes.setTransparencyMode(TransparencyAttributes.BLENDED);
		transparencyAttributes.setSrcBlendFunction(TransparencyAttributes.BLEND_SRC_ALPHA);
	}

	private static Map<HomePieceOfFurniture,PasteEntry> copyPasteMap = Collections.synchronizedMap(new Hashtable<HomePieceOfFurniture,PasteEntry>());
	private Map<HomePieceOfFurniture3D, Object> pieceSelectionMap = new ConcurrentHashMap<HomePieceOfFurniture3D, Object>();
	
	private PlatformGeometry platformGeometry;
	private HoverController hoverController;
	private FurnitureUtils furnitureUtils;
	private PickInfo lastWallPick = null;
	private HomePieceOfFurniture homePiece = null;
	private Point lastMousePoint = new Point();

	private Thread redrawActionThread;
	private boolean isInsideHeadUpDisplay;
	private JPopupMenu popUpMenu;
	private DelayedAction redrawAction;
	
	private Runnable updateScreen = new Runnable() {
		public void run()
		{
			revalidate();
			requestFocus();
		}
	};

	private static class PasteEntry
	{
		private static enum ORIGIN { COPY_OPERATION, CUT_OPERATION };
		private Object item;
		private BranchGroup itemPreview;
		private ORIGIN origin;
		public PasteEntry(Object item,BranchGroup itemPreview,ORIGIN origin)
		{
			this.item = item;
			this.itemPreview = itemPreview;
			this.origin = origin;
		}
		public Object getItem()
		{
			return item;
		}
		public BranchGroup getItemPreview()
		{
			return itemPreview;
		}
		public ORIGIN getOrigin()
		{
			return origin;
		}
	}
	private void showPopUp(MouseEvent mouseReleased)
	{
		if (popUpMenu != null && popUpMenu.isVisible())
			return;
		popUpMenu = new RightClickMenu(CustomHomeComponent3D.this).buildMenu();
		popUpMenu.show(mouseReleased.getComponent(), mouseReleased.getX(), mouseReleased.getY());
	}
	
	/**
	 *	-- START -- Mouse motions listener and processor class -- 
	 */
	private class CustomMouseInputAdapter extends HomeComponentMouseAdapter
	{
		private Point point;
		private TimedMouseEventHandler dragHandler;
		private TimedMouseEventHandler moveHandler;
		
		public CustomMouseInputAdapter(HomeController3D controller)
		{
			super(CustomHomeComponent3D.this, controller);
			furnitureUtils = new FurnitureUtils(home);
		}
		
		@Override
		public void mouseEntered(MouseEvent mouseEntered)
		{
			updateMousePosition(mouseEntered);
		}
		
		@Override
		public void mouseExited(final MouseEvent mouseExited)
		{
			if (redrawAction != null)
				redrawAction.disarm();
			new Thread("MouseExited") { public void run() {
				try
				{
					sleep(50L);
				}
				catch(InterruptedException ie)
				{
					return;
				}
				hoverController.cancelHover();
			}}.start();
		}

		@Override
		public void mousePressed(MouseEvent mousePressed)
		{
			if (redrawAction != null)
				redrawAction.disarm();
			
			if (mousePressed.isPopupTrigger())
				return;
			
			boolean isControl = mousePressed.isControlDown();
			point = mousePressed.getPoint();
			try
			{
				vpPicker.setShapeLocation(mousePressed);
				PickInfo pickInfo = vpPicker.pickClosest();
				if(pickInfo != null)
				{
					Node picked = pickInfo.getNode();
					if(picked instanceof ImagePlaneShape3D)
					{
						hoverController.cancelHover();
					}
					else if(picked instanceof Shape3D)
					{
						if(hoverController.isActive())
						{
							// act's like more...
							Object params[] = {hoverController.hoveringOver().getName(), hoverController.hoveringOver().getObjectId()};
							logger.log(Level.INFO, hoverController.hoveringOver().getName() + ", " + hoverController.hoveringOver().getObjectId());
							fireGestureRecognizedEvent(new ActivationGestureEvent(params, "hoverArming", null));
						}
					}
					return;
				}
				
				HomePieceOfFurniture pieceOfFurniture = pickPieceOfFurnitureNearestMousePosition(point);
				boolean isHomeFilePiece = pieceOfFurniture instanceof Selectable3D;
				if (isHomeFilePiece || (null != pieceOfFurniture && pieceOfFurniture.isDoorOrWindow()))
				{
					selectFurniturePiece(mousePressed);
					homePiece = pieceOfFurniture;
					if (!isControl && isHomeFilePiece && 2 == mousePressed.getClickCount())
					{
						fireActivationEvent(((Selectable3D)pieceOfFurniture).getData());
					}
					else
					{
						lastWallPick = null;
						logger.log(Level.WARNING, "wall object down: " + homePiece.getName());
						moveFurnitureToMousePositionOnWall(mousePressed);
					}
				}
				else if (null != pieceOfFurniture)
				{
					selectFurniturePiece(mousePressed);
					homePiece = pieceOfFurniture;
					lastWallPick = null;
					logger.log(Level.WARNING, "floor object down: " + homePiece.getName());
					homePiece = pieceOfFurniture;
					moveFurnitureToMousePositionOnFloor(mousePressed.getPoint());
				}
				else if (!isControl)
				{
					removeSelectionFromCurrentlySelectedPieces();
					super.mousePressed(mousePressed);
					point = null;
				}
			}
			catch (Exception e)
			{
				logger.log(Level.FINE, "Error processing response",  e);
			}
		}

		@Override
		public void mouseReleased(MouseEvent mouseReleased)
		{
			if (mouseReleased.isPopupTrigger())
			{
				showPopUp(mouseReleased);
				return;
			}
			if (homePiece != null)
				logger.log(Level.FINE, "down: " + homePiece.getName());
			else
			  super.mouseReleased(mouseReleased);
			
			homePiece = null;
		}

		@Override
		public void mouseDragged(MouseEvent mouseDragged)
		{
			if(dragHandler == null) dragHandler = new TimedMouseEventHandler(new TimedMouseEventHandlerCallback()
			{
				@Override
				public void handle(MouseEvent dragged)
				{
					_mouseDragged(dragged);
				}
			},33L);
			dragHandler.update(mouseDragged);
		}
		
		@Override
		public void mouseMoved(final MouseEvent mouseMoved)
		{
			boolean foundHeadsUpDisplay = false;
			try 
			{
				PickInfo p = null;
				if (vpPicker != null && (p = vpPicker.pickAny()) != null)
				{
					for (Node node = p.getNode(); node != null; node = node.getParent())
						if (node instanceof HeadsUpDisplay)
						{
							if (!isInsideHeadUpDisplay && (popUpMenu == null || !popUpMenu.isVisible()))
							{							
								if (redrawActionThread == null || !redrawActionThread.isAlive())
									(redrawActionThread = new Thread(redrawAction  = new DelayedAction(new Runnable(){
										public void run(){
											CustomHomeComponent3D.this.showPopUp(mouseMoved);
										}}, 100))).start();
								isInsideHeadUpDisplay = true;
							}
							foundHeadsUpDisplay = true;
						}
				}
			} 
			catch (IllegalThreadStateException e) 
			{
				logger.log(Level.WARNING, e.getMessage());
			}
			if (!foundHeadsUpDisplay && redrawAction != null)
			{
				isInsideHeadUpDisplay = false;
				redrawAction.disarm();
				if (popUpMenu != null && popUpMenu.isVisible())
				{
					popUpMenu.setVisible(false);
					popUpMenu = null;
				}
				redrawAction = null;
			}
			if(moveHandler == null) moveHandler = new TimedMouseEventHandler(new TimedMouseEventHandlerCallback()
			{
				@Override
				public void handle(MouseEvent moved)
				{
					_mouseMoved(moved);
				}
			},33L);
			moveHandler.update(mouseMoved);
		}
		
		private void _checkIsEventDispatchThread()
		{
			if(!EventQueue.isDispatchThread())
				throw new IllegalThreadStateException("calling thread must be event dispatch thread");
		}
		
		private void _mouseDragged(MouseEvent mouseDragged)
		{
			_checkIsEventDispatchThread();
			hoverController.cancelHover();
			boolean isWallOrientedPiece = homePiece instanceof Selectable3D;
			if (isWallOrientedPiece || (null != homePiece && homePiece.isDoorOrWindow()))
			{
				moveFurnitureToMousePositionOnWall(mouseDragged);
				updateMousePosition(mouseDragged);
			}
			else if (null != homePiece)
			{
				moveFurnitureToMousePositionOnFloor(mouseDragged.getPoint());
				updateMousePosition(mouseDragged);
			}
			else if(pieceSelectionMap.isEmpty())
			{
				super.mouseDragged(mouseDragged);
			}
		}
		
		private void _mouseMoved(MouseEvent mouseMoved)
		{
			_checkIsEventDispatchThread();
			point = mouseMoved.getPoint();
			lastMousePoint.setLocation(mouseMoved.getPoint());
			
			if(!mouseMoved.isControlDown() && (dragHandler == null || !dragHandler.isActive()))
				if (vpPicker.getBranchGroup().isLive())
				{
					vpPicker.setShapeLocation(mouseMoved);
					PickInfo pickInfo = vpPicker.pickClosest();
					if(pickInfo != null && pickInfo.getNode() instanceof PlaneShape3D)
					{
						hoverController.activate();
					}
					else
						checkHover(pickInfo, true);
				}
			updateMousePosition(mouseMoved);
		}
	}
	
	private void checkHover(PickInfo pickInfo, boolean ignoreBanners)
	{
		HomePieceOfFurniture picked = pickPieceOfFurnitureNearestMousePosition(lastMousePoint, ignoreBanners);
		hoverController.update(pickInfo, picked, lastMousePoint);
	}

	private boolean processRotateEvent(boolean clockwise)
	{
		boolean rotatePerformed = false;
		for (HomePieceOfFurniture3D branchGroup : pieceSelectionMap.keySet())
		{
			HomePieceOfFurniture homePiece = (HomePieceOfFurniture) branchGroup.getUserData();
			if(!homePiece.isDoorOrWindow() && !(homePiece instanceof Selectable3D))
			{
				float angle = homePiece.getAngle();
				angle += clockwise?-0.157:0.157;
				home.setPieceOfFurnitureAngle(homePiece, angle);
			}
			rotatePerformed = true;
		}
		return rotatePerformed;
	}
	
	private void processCopyCut(ActionEvent e, PasteEntry.ORIGIN origin)
	{
		synchronized(pieceSelectionMap)
		{				
			List<BranchGroup> branchesForClipboard;
			if ((e.getModifiers() & ActionEvent.CTRL_MASK) == 0)
			{
				branchesForClipboard = new ArrayList<BranchGroup>();
				copyPasteMap.clear();
			}
			else
			{
				branchesForClipboard = new ArrayList<BranchGroup>(Arrays.asList(clipboardContentsViewer.getModel().getSelection()));
			}
			for(HomePieceOfFurniture3D branch: pieceSelectionMap.keySet())
			{
				Object userData = branch.getUserData();
				if(userData instanceof HomePieceOfFurniture)
				{
					HomePieceOfFurniture3D newBranch = null;
					HomePieceOfFurniture homePiece = (HomePieceOfFurniture) userData;
					HomePieceOfFurniture newHomePiece = null;
					if(homePiece instanceof HomeFilePieceOfFurniture)
					{
						File originalFile = ((HomeFilePieceOfFurniture)homePiece).getFile();
						try
						{
							File newFile = null;
							String newName = originalFile.getName();
							do
							{
								newName = FilenameGeneratorForCopies.getNextFileCopyName(newName);
								newFile = fileBuilder.build(originalFile.getParentFile(), newName);
							} while(newFile.exists());
							newHomePiece = new HomeFilePieceOfFurniture(homePiece,newFile);
							newBranch = createHomePieceOfFurniture3D(new HomeFilePieceOfFurniture((HomeFilePieceOfFurniture)homePiece));
						}
						catch(IOException ioe)
						{
							throw new RuntimeException(ioe);
						}
					}
					else
					{
						newHomePiece = new HomePieceOfFurniture(homePiece);
						newBranch = createHomePieceOfFurniture3D(new HomePieceOfFurniture(homePiece));
					}
					branchesForClipboard.add(newBranch);
					copyPasteMap.put(homePiece, new PasteEntry(newHomePiece,newBranch,origin));
				}
			}
			clipboardContentsViewer.getModel().setSelection(branchesForClipboard.toArray(new BranchGroup[0]));
		}
	}

	private void processCopyEvent(ActionEvent e)
	{
		logger.warning("copy");
		processCopyCut(e,PasteEntry.ORIGIN.COPY_OPERATION);
	}
	
	private void processCutEvent(ActionEvent e)
	{
		logger.warning("cut");
		processCopyCut(e,PasteEntry.ORIGIN.CUT_OPERATION);
	}

	private void processNewBannerEvent()
	{
		PickInfo wallPick = pickFacingWall(lastMousePoint);
		if (wallPick != null)
		{
			Wall wall = (Wall)wallPick.getNode().getParent().getUserData();
			
			BannerPieceOfFurniture newPiece = (BannerPieceOfFurniture)new BannerPieceOfFurnitureBuilder(
					"<html><body style=\"text-color: #0;\">double-click<br>to <font color=red><u>edit</u></font></body></html>").build();
			BannerStringPieceOfFurniture homePiece = new BannerStringPieceOfFurniture(newPiece);
			home.addPieceOfFurniture(homePiece);

			furnitureUtils.placeFurnitureOnWall(homePiece, wall, wallPick.getClosestIntersectionPoint());
		}
	}
	
	private void processDeleteEvent(ActionEvent e)
	{
		logger.warning("delete");
		ArrayList<File> files = new ArrayList<File>();
		for (HomePieceOfFurniture3D branchGroup : pieceSelectionMap.keySet())
		{
			HomePieceOfFurniture homePiece = (HomePieceOfFurniture) branchGroup.getUserData();
			if(homePiece instanceof HomeFilePieceOfFurniture)
				files.add(((HomeFilePieceOfFurniture)homePiece).getFile());
		}
		if (files.size() > 0)
		{
			boolean containsDirs = false;
			StringBuffer sb = new StringBuffer("Delete '");
			for (int k = 0; k < files.size(); k++)
			{
				sb.append('\'').append(files.get(k).getName()).append('\'');
				if (k > 0)
					sb.append(", ");
				sb.append(CR);
				if (k > 9 && files.size() > 10)
				{
					sb.append("   ...(").append(files.size()).append(" files)...");
					break;
				}
				if (files.get(k).isDirectory())
					containsDirs = true;
			}
			if (containsDirs)
				sb.append(" and all of ").append(files.size()>1?"their":"its").append(" contents");
			sb.append('?');
			if (JOptionPane.showConfirmDialog(CustomHomeComponent3D.this, sb.toString(),
					"Delete", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION)
			{
				return;  // delete cancelled
			}	
			for (File file: files)
				fileBuilder.delete(file);}
		for (HomePieceOfFurniture3D branchGroup : pieceSelectionMap.keySet())
			home.deletePieceOfFurniture((HomePieceOfFurniture) branchGroup.getUserData());
		pieceSelectionMap.clear();
	}

	private void processPasteEvent(ActionEvent e)
	{
		logger.warning("paste");
		synchronized(copyPasteMap)
		{
			for(HomePieceOfFurniture piece: copyPasteMap.keySet())
			{
				final PasteEntry pasteEntry = copyPasteMap.get(piece);
				Object toPaste = pasteEntry.getItem();
				if(toPaste instanceof HomePieceOfFurniture)
				{
					HomePieceOfFurniture homePieceToPaste;
					if(toPaste instanceof HomeFilePieceOfFurniture)
						homePieceToPaste = new HomeFilePieceOfFurniture((HomeFilePieceOfFurniture) toPaste);
					else
						homePieceToPaste = new HomePieceOfFurniture((HomePieceOfFurniture) toPaste);
					
					home.addPieceOfFurniture(homePieceToPaste);
					if(homePieceToPaste instanceof HomeFilePieceOfFurniture)
					{
						// TODO perhaps this is best executed as a "Copy Gesture Event"
						// for now it's a direct file-2-file copy with no cancel ability
						final HomeFilePieceOfFurniture copyToPiece = (HomeFilePieceOfFurniture) homePieceToPaste;
						final HomeFilePieceOfFurniture copyFromPiece = (HomeFilePieceOfFurniture) piece;
						
						executor.execute(new Runnable() { public void run() {	
							InputStream copyFromStream = null;
							OutputStream copyToStream = null;
							File copyToFile = null;
							File copyFromFile = null;
							try
							{
								copyToFile = copyToPiece.getFile();
								copyFromFile = copyFromPiece.getFile();
								
								if(!copyToFile.exists() && !copyToFile.createNewFile())
									throw new IOException("could not create file:"+copyToFile);
								
								copyFromStream = fileBuilder.buildFileInputStream(copyFromFile);
								copyToStream = fileBuilder.buildFileOutputStream(copyToFile);
								
								byte[] readbuf = new byte[8192];
								int numBytesReadIntoReadBuffer = 0;
								while(-1 != (numBytesReadIntoReadBuffer = copyFromStream.read(readbuf)))
								{
									copyToStream.write(readbuf,0,numBytesReadIntoReadBuffer);
								}
								FilePieceOfFurnitureTextureManager.getInstance().duplicateThumbnail(copyFromFile, copyToFile);
								if(PasteEntry.ORIGIN.CUT_OPERATION == pasteEntry.getOrigin())
								{
									if(!fileBuilder.delete(copyFromFile))
										logger.warning("could not delete cut file after paste");
								}
							}
							catch(IOException ioe)
							{
								logger.severe(String.format("error while copying [%s] to [%s]",copyFromFile,copyToFile));
							}
							finally
							{
								try { copyFromStream.close(); } catch(IOException ioe) {}
								try { copyToStream.close(); } catch(IOException ioe) {}
							}
						}});
						moveFurnitureToMousePositionOnWall(lastMousePoint,homePieceToPaste);
					}
					else
					{
						lastWallPick = pickGround(lastMousePoint);
						if (homePieceToPaste.isDoorOrWindow())
							moveFurnitureToMousePositionOnWall(lastMousePoint, homePieceToPaste);
						else
						{
							moveFurnitureToMousePositionOnFloor(lastMousePoint, homePieceToPaste);
							PickInfo picks[] = roomPicker.pickAll();
							for (PickInfo pick: picks)
								for (Node node = pick.getNode(); node != null; node = node.getParent())
								{
									if (node instanceof HomePieceOfFurniture3D)
										if (homePieceToPaste.equals(((HomePieceOfFurniture3D)node).getUserData()))
										{
											showFurniturePieceSelection(((HomePieceOfFurniture3D)node));			
											setColorBasedOnIntersection(homePieceToPaste);
										}
								}
						}
					}
				}
			}
		}
	}

	private void processUndoEvent(ActionEvent e)
	{
		logger.warning("undo");
		synchronized(copyPasteMap)
		{
			for(PasteEntry pasteEntry: copyPasteMap.values())
			{
				pasteEntry.getItemPreview().detach();
			}
			copyPasteMap.clear();
		}
		clipboardContentsViewer.getModel().setSelection(null);	
	}
	
	private void moveFurnitureToMousePositionOnWall(Point mouseLocation,HomePieceOfFurniture piece)
	{
		PickInfo wallPick = pickFacingWall(mouseLocation);
		if (wallPick != null && piece != null)
		{
			Wall wall = (Wall)wallPick.getNode().getParent().getUserData();
			furnitureUtils.placeFurnitureOnWall(piece, wall, wallPick.getClosestIntersectionPoint());
		}
	}

	private void moveFurnitureToMousePositionOnWall(MouseEvent mouseEvent)
	{
		PickInfo wallPick = pickFacingWall(mouseEvent.getPoint());
		if (wallPick != null && homePiece != null)
		{
			Wall wall = (Wall)wallPick.getNode().getParent().getUserData();
			Point3d newWallPick = wallPick.getClosestIntersectionPoint();
			if (lastWallPick != null)
			{
				Wall lastWall = (Wall)lastWallPick.getNode().getParent().getUserData();
				for (HomePieceOfFurniture3D branchGroup : pieceSelectionMap.keySet())
				{
					HomePieceOfFurniture homePiece = (HomePieceOfFurniture) branchGroup.getUserData();
					if (lastWall.equals(wall))
						furnitureUtils.moveFurnitureOnWall(homePiece, wall, newWallPick,
								lastWallPick.getClosestIntersectionPoint());
					else
					{
						furnitureUtils.placeFurnitureOnWall(homePiece, wall, newWallPick);
					}
				}
			}
			else
			{
				logger.log(Level.WARNING, "wallpick == null");
			}
			lastWallPick = wallPick;
		}
	}
	private boolean moveFurnitureToMousePositionOnFloor(Point point, HomePieceOfFurniture pieceOfFurniture)
	{
		PickInfo groundPick = pickGround(point);
		if (groundPick != null)
		{
			furnitureUtils.placeFurnitureOnFloor(pieceOfFurniture, groundPick.getClosestIntersectionPoint());
			return true;
		}
		return false;
	}
	
	private boolean moveFurnitureToMousePositionOnFloor(Point point)
	{
		PickInfo groundPick = pickGround(point);
		if (groundPick != null)
		{
			Point3d newWallPick = groundPick.getClosestIntersectionPoint();
			if (lastWallPick != null)
			{
				Point3d wallPoint = lastWallPick.getClosestIntersectionPoint();
				for (HomePieceOfFurniture3D branchGroup : pieceSelectionMap.keySet())
				{
					HomePieceOfFurniture homePiece = (HomePieceOfFurniture) branchGroup.getUserData();
					furnitureUtils.moveFurnitureOnFloor(homePiece, newWallPick,	wallPoint);
					System.err.print("moveFurnitureToMousePositionOnFloor homePiece = " + homePiece);
					if (CollisionUtils.testForIntersectionWithWalls(home.getWalls(), homePiece))
					{
						furnitureUtils.moveFurnitureOnFloor(homePiece, wallPoint, newWallPick);
					}
					else
					{
						lastWallPick = groundPick;
						setColorBasedOnIntersection(homePiece);
					}
				}
			}
			else
				lastWallPick = groundPick;
			return true;
		}
		return false;
	}

	private void setColorBasedOnIntersection(HomePieceOfFurniture homePiece)
	{
		boolean collision = CollisionUtils.testForIntersectionWithPieces(home.getFurniture(), homePiece).hasNext();
		for (HomePieceOfFurniture3D piece3d: pieceSelectionMap.keySet())
			if (piece3d.getUserData() == homePiece)
			{
				BranchGroup branchGroup = getSelectionBranchGroupIfSelected(piece3d);
				for (Enumeration<?> e = branchGroup.getAllChildren(); e.hasMoreElements();)
				{
					Object child = e.nextElement();
					if (child instanceof Box)
						((Box)child).getAppearance().getMaterial().setDiffuseColor(new Color3f(collision?LIGHT_PINK:LIGHT_BLUE));
				}
			}
	}
	
//	public JPopupMenu getComponentPopupMenu()
//	{
//		return new RightClickMenu(this).buildMenu();
//	}
	
	/**
	 *	-- END -- -- 
	 */
	
	@Override
	protected void deinitializeComponent(Canvas3D canvas3D,Home home)
	{
		super.deinitializeComponent(canvas3D, home);
		FolderMonitor.getInstance().cancel();
		FilePieceOfFurnitureTextureManager.getInstance().cancel();
		FilePieceOfFurnitureTextureManager.getInstance()
				.removeFileThumbnailChangeListener(fileThumbnailTextureLoader);
		FolderMonitor.getInstance().removeListener(fileThumbnailTextureLoader);
		FolderMonitor.getInstance().removeListener(folderChangeListener);
		
		universe.getViewer().getView().removeAllCanvas3Ds();
		universe.cleanup();
		universe = null;
		headsUpDisplay = null;
		clipboardContentsViewer = null;
		//fileFurniturePieceMap = null;
		fileFurniturePieceMap.clear();
		bannerFurniturePieceMap.clear();
		hoverController.cancelHover();
	}
	
	@Override
	protected void initializeComponent(final Canvas3D canvas3D,Home home)
	{
		super.initializeComponent(canvas3D, home);
		for(Object o : VirtualUniverse.getProperties().entrySet())
			System.err.println("o = "+o);
		headsUpDisplay = new HeadsUpDisplay(canvas3D, universe);
		clipboardContentsViewer = headsUpDisplay.getClipboardContentsViewer();
		clipboardContentsViewer.getModel().setSelection(getCopyBufferBranchGroup());
		headsUpDisplay.setCurrentAddress(home.getName());
		headsUpDisplay.addCancelButtonListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e)
			{
				int answer = JOptionPane.showConfirmDialog(canvas3D, "Are you sure you want to cancel the transfer process?",
						"Rooms3d", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
				if(answer == JOptionPane.YES_OPTION)
					transferCancelled.set(true);
			}
		});
		
		platformGeometry = new PlatformGeometry();
		platformGeometry.setCapability(PlatformGeometry.ALLOW_CHILDREN_EXTEND);
		platformGeometry.setCapability(PlatformGeometry.ALLOW_CHILDREN_WRITE);
		platformGeometry.addChild(headsUpDisplay);
		
		hoverController = new HoverController((Canvas3D)getComponent(0),platformGeometry);
		
		universe.getViewingPlatform().setPlatformGeometry(platformGeometry);
		Enumeration<?> universeBGs = universe.getLocale().getAllBranchGraphs();
		universeBGs.nextElement();
		roomPicker = new PickCanvas(canvas3D, (BranchGroup) universeBGs.nextElement());
		roomPicker.setFlags(PickInfo.NODE | PickInfo.CLOSEST_INTERSECTION_POINT);
		roomPicker.setMode(PickInfo.PICK_GEOMETRY);
		
		vpPicker = new PickCanvas((Canvas3D)getComponent(0),universe.getViewingPlatform());
		vpPicker.setFlags(PickInfo.NODE);
		
		fileThumbnailTextureLoader = new FileThumbnailTextureLoader(canvas3D);
		synchronized (fileThumbnailTextureLoaderLock)
		{
			logger.log(Level.INFO, "notify texture loader available");
			fileThumbnailTextureLoaderLock.notifyAll();
		}
		FolderMonitor.getInstance().addListener(fileThumbnailTextureLoader);
		FilePieceOfFurnitureTextureManager.getInstance().addFileThumbnailChangeListener(
				fileThumbnailTextureLoader);
		EventQueue.invokeLater(updateScreen);
	}

	public CustomHomeComponent3D(final Home home, CustomHomeController3D controller)
	{
		super(home, controller);
		setPreferredSize(new Dimension(800, 600));
		installKeyboardActions();
		createActions(controller);
		fileFurniturePieceMap = 
			Collections.synchronizedMap(new Hashtable<File, HomeFilePieceOfFurniture>());
		bannerFurniturePieceMap = 
			Collections.synchronizedMap(new Hashtable<StringBuffer, Texturable>());
		
		EventQueue.invokeLater(updateScreen);
		
		//setComponentPopupMenu(new JPopupMenu()); // no NPE on right-click
		setDropTarget(new DropTarget(this, new CustomHomeComponent3DDropTargetAdapter(home) {
			@Override
			protected PickInfo updateWallPickInfoFromDrag(DropTargetDragEvent dtde)
			{
				return pickFacingWall(dtde.getLocation());
			}

			@Override
			public void drop(DropTargetDropEvent dtde)
			{
				HomeFilePieceOfFurniture homeFilePiece = getHomeFilePieceOfFurniture();
				if (null != homeFilePiece)
				{
					ImportGestureEventCallbackImpl importGestureEventCallback = 
						new ImportGestureEventCallbackImpl(homeFilePiece);
					fireGestureRecognizedEvent(new ImportGestureEvent(this,
							homeFilePiece.getFile(), importGestureEventCallback));
					dtde.dropComplete(true);
				}
				else
					dtde.dropComplete(false);
				super.drop(dtde);
			}
		}));
		this.home = home;
		folderChangeListener = new FolderChangeListener() {
			@Override
			public void filesAdded(File[] added)
			{
				Collection<Wall> wallCollection = home.getWalls();
				Wall[] walls = wallCollection.toArray(new Wall[wallCollection.size()]);
				for (File file : added)
				{
					if (fileFurniturePieceMap.containsKey(file))
						continue;
					Wall wall = walls[(int)Math.floor(Math.random() * walls.length)];
					float wvx = wall.getXEnd() - wall.getXStart();
					float wvy = wall.getYEnd() - wall.getYStart();
					float wallLength = (float)Math.sqrt(wvx * wvx + wvy * wvy);
					float wallHeight = wall.getHeight();
					final Random2DArrangement random2DArrangement = new Random2DArrangement(
							wallLength, wallHeight);
					FilePieceOfFurniture newPiece = (FilePieceOfFurniture)new FilePieceOfFurnitureBuilder(
							file).build();
					HomeFilePieceOfFurniture homePiece = new HomeFilePieceOfFurniture(newPiece);
					fileFurniturePieceMap.put(file, homePiece);
					home.addPieceOfFurniture(homePiece);
					random2DArrangement.add(new HomeFilePieceOfFurnitureArrangeableSupport(home,
							wall, homePiece));
					EventQueue.invokeLater(new Runnable() {
						public void run()
						{
							random2DArrangement.arrange();
						}
					});
				}
			}

			@Override
			public void filesModified(File[] modified)
			{
			}

			@Override
			public void filesRemoved(File[] removed)
			{
				for (int i = 0; i < removed.length; i++)
				{
					File toRemove = removed[i];
					if (fileFurniturePieceMap.containsKey(toRemove))
					{
						home.deletePieceOfFurniture(fileFurniturePieceMap.remove(toRemove));
						try
						{
							FilePieceOfFurnitureTextureManager.getInstance().removeThumbnail(toRemove);
							fileFurniturePieceMap.remove(toRemove);
						}
						catch (ThumbnailException e)
						{
							String msg = String.format("couldn't delete thumbnail, file[%s]",
									toRemove);
							logger.log(Level.WARNING, msg, e);
						}
					}
				}
			}
		};
		FolderMonitor.getInstance().addListener(folderChangeListener);
	}
	public String getHomeName()
	{
		return home.getName();
	}
	void fireActivationEvent(Object file)
	{
			ActivationGestureCallback activationGestureCallback = 
				new ActivationGestureCallback() {
				@Override
				public Status reportError(Object target, Error error, Object report)
				{
					return Status.INDETERMINATE;
				}

				@Override
				public Status reportProgress(int totalPercents,
						int currentPercents, Object currentTarget)
				{
					File targetFile = (File)currentTarget;
					
					if(null == headsUpDisplay)
					{
						return Status.ABORT;
					}
					
					headsUpDisplay.getModel().setCurrentFile("Reading '" + targetFile.getName() + "'");
					headsUpDisplay.setProgress(currentPercents);
					
					if(currentPercents == 100)
					{
						headsUpDisplay.getModel().setShowProgress(false);
					}
					
					if(transferCancelled.get())
					{
						transferCancelled.set(false);
						headsUpDisplay.getModel().setShowProgress(false);
						return Status.ABORT;
					}
					
					return Status.PROCEED;
				}

				@Override
				public Status reportWait(Object currentTarget)
				{
					if(null == headsUpDisplay)
					{
						return Status.ABORT;
					}
					
					headsUpDisplay.getModel().setCurrentFile("Please wait");

					if(transferCancelled.get())
					{
						transferCancelled.set(false);
						headsUpDisplay.getModel().setShowProgress(false);
						return Status.ABORT;
					}

					return Status.PROCEED;
				}

				@Override
				public Status reportCompleted(Object currentTarget)
				{
					if (currentTarget instanceof StringBuffer)
					{
						return Status.ABORT;
					}

					if(null == headsUpDisplay)
					{
						return Status.ABORT;
					}
					transferCancelled.set(false);
					if(currentTransferTarget.get() == currentTarget)
						headsUpDisplay.getModel().setShowProgress(false);
					return Status.PROCEED;
				}

				@Override
				public void activationComplete(Object toActivate)
				{
					if(null == headsUpDisplay)
						return;
					logger.warning(" Activation completed for: [" + toActivate
							+ "]");
					if(currentTransferTarget.get() == toActivate)
						headsUpDisplay.getModel().setShowProgress(false);
				}

				@Override
				public void activationStarting(Object toActivate)
				{	
					if(null == headsUpDisplay)
						return;
					if (toActivate instanceof File)
					{
						File targetFile = (File)toActivate;
						currentTransferTarget.set(toActivate);
						headsUpDisplay.getModel().setShowProgress(true);
						headsUpDisplay.setProgress(0);
						headsUpDisplay.getModel().setCurrentFile("Reading '" + targetFile.getName() + "'");
					}
				}

				/**
				 * @param currentTarget - File to be edited
			 	 * @param argument - local File copy
				 */
				@Override
				public void notifyBeforeCompleted(Object currentTarget,
						Object argument)
				{
					if (currentTarget instanceof StringBuffer)
					{
						Texturable bannerPiece = bannerFurniturePieceMap.get(currentTarget);
						waitForfileThumbnailTextureLoader().manageTexture((BranchGroup)bannerPiece);
						return;
					}
					if(null == headsUpDisplay)
						return;
					File localFile = (File)argument;
					FilePieceOfFurnitureTextureManager.getInstance().updateThumbnailFromNativeFile(localFile,
							(File)currentTarget);
				}
			};
			fireGestureRecognizedEvent(new ActivationGestureEvent(this,
					file, activationGestureCallback));
	}

	private Object existingArrowLeft;
	private Object existingArrowRight;

	private void installKeyboardActions()
	{
		InputMap inputMap = getInputMap(WHEN_FOCUSED);
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK),
				ActionType.COPY_OPERATION);
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK),
				ActionType.PASTE_OPERATION);
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK),
				ActionType.UNDO_OPERATION);
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK),
				ActionType.CUT_OPERATION);
		inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
				ActionType.DELETE_OPERATION);
		existingArrowLeft = inputMap.get(KeyStroke.getKeyStroke("LEFT"));
		existingArrowRight = inputMap.get(KeyStroke.getKeyStroke("RIGHT"));
		inputMap.put(KeyStroke.getKeyStroke("LEFT"), ActionType.ARROW_LEFT);
		inputMap.put(KeyStroke.getKeyStroke("RIGHT"), ActionType.ARROW_RIGHT);
	}

	protected MouseInputAdapter createMouseInputAdapter(final HomeController3D controller)
	{
		mouseInputAdapter = new CustomMouseInputAdapter(controller);
		return mouseInputAdapter;
	}
	
	@SuppressWarnings("serial")
	private void createActions(HomeController3D controller)
	{
		final ActionMap actionMap = getActionMap();
		actionMap.put(ActionType.COPY_OPERATION, new AbstractAction("Copy") {
			@Override
			public void actionPerformed(ActionEvent e)
			{
				processCopyEvent(e);
			}
		});
		actionMap.put(ActionType.CUT_OPERATION, new AbstractAction("Cut") {
			@Override
			public void actionPerformed(ActionEvent e)
			{
				processCutEvent(e);
			}
		});
		actionMap.put(ActionType.PASTE_OPERATION, new AbstractAction("Paste") {
			@Override
			public void actionPerformed(ActionEvent e)
			{
				processPasteEvent(e);
			}
		});
		actionMap.put(ActionType.UNDO_OPERATION, new AbstractAction("Undo") {
			@Override
			public void actionPerformed(ActionEvent e)
			{
				processUndoEvent(e);
			}
		});
		actionMap.put(ActionType.DELETE_OPERATION, new AbstractAction("Delete") {
			@Override
			public void actionPerformed(ActionEvent e)
			{
				processDeleteEvent(e);
			}
		});
		actionMap.put(ActionType.ARROW_LEFT, new AbstractAction("ArrowLeft") {
			private Action prevArrowLeft = existingArrowLeft != null?actionMap
					.get(existingArrowLeft):null;

			@Override
			public void actionPerformed(ActionEvent e)
			{
				logger.warning("Arrow-Left, " + e.getModifiers());
				if (!processRotateEvent(true) && prevArrowLeft != null)
					prevArrowLeft.actionPerformed(e);
			}
		});
		actionMap.put(ActionType.ARROW_RIGHT, new AbstractAction("ArrowRight") {
			private Action prevArrowRight = existingArrowRight != null?actionMap
					.get(existingArrowRight):null;

			@Override
			public void actionPerformed(ActionEvent e)
			{
				logger.warning("Arrow-Right, " + e.getModifiers());
				if (!processRotateEvent(false) && prevArrowRight != null)
					prevArrowRight.actionPerformed(e);
			}
		});
		actionMap.put(ActionType.NEW_BANNER, new AbstractAction("NewBanner") {
			@Override
			public void actionPerformed(ActionEvent e)
			{
				logger.warning("New-Banner, " + e.getModifiers());
				processNewBannerEvent();
			}
		});
	}

	public BranchGroup[] getCopyBufferBranchGroup()
	{
		ArrayList<BranchGroup> branchesForClipboard = new ArrayList<BranchGroup>(); 
		ArrayList<HomePieceOfFurniture> oldKeys = new ArrayList<HomePieceOfFurniture>(copyPasteMap.keySet());
		for(HomePieceOfFurniture homePiece: oldKeys)
		{
			PasteEntry previous = copyPasteMap.get(homePiece);	
			HomePieceOfFurniture newHomePiece = new HomePieceOfFurniture(homePiece);
			BranchGroup newBranch = createHomePieceOfFurniture3D(new HomePieceOfFurniture(newHomePiece));
			branchesForClipboard.add(newBranch);

			copyPasteMap.put(newHomePiece, new PasteEntry(newHomePiece,newBranch, previous.getOrigin()));
		}
		for(HomePieceOfFurniture homePiece: oldKeys)
			copyPasteMap.remove(homePiece);
		
		return branchesForClipboard.toArray(new BranchGroup[0]);
	}
	@Override
	protected HomePieceOfFurniture3D createHomePieceOfFurniture3D(HomePieceOfFurniture piece)
	{
		boolean isBanner = piece instanceof BannerStringPieceOfFurniture;
		if (piece instanceof HomeFilePieceOfFurniture)
		{
			final HomeFilePieceOfFurniture homePiece = (HomeFilePieceOfFurniture)piece;
			if(homePiece.getFile() != null && !fileFurniturePieceMap.containsKey(homePiece.getFile()))
			{
					fileFurniturePieceMap.put(homePiece.getFile(), homePiece);
					FolderMonitor.getInstance().preload(homePiece.getFile());
			}
			else
			{
				logger.fine("ignoring add for map, likely from cut and paste");
			}
		}
		else if (!isBanner)
		{
			return super.createHomePieceOfFurniture3D(piece);
		}
		final TexturablePieceOfFurniture3D homeFilePieceOfFurniture3D = new TexturablePieceOfFurniture3D(piece);
		homeFilePieceOfFurniture3D.setHome(home);
		if (isBanner)
			bannerFurniturePieceMap.put((StringBuffer)((BannerStringPieceOfFurniture)piece).getData(), homeFilePieceOfFurniture3D);
		
		executor.execute(new Runnable() {
			public void run()
			{
				waitForfileThumbnailTextureLoader().manageTexture(homeFilePieceOfFurniture3D);
			}
		});
		return homeFilePieceOfFurniture3D;
	}

	/**
	 * Used to determine which wall to attach drop file to and at what 3D point
	 * along that wall corresponds to supplied 2D dropPoint.
	 * 
	 * @see CustomHomeComponent3DDropTargetAdapter
	 * @param dropPoint
	 * @return
	 */
	private PickInfo pickFacingWall(java.awt.Point dropPoint)
	{
		PickInfo ans = null;
		roomPicker.setShapeLocation(dropPoint.x, dropPoint.y);
		PickInfo[] picks = roomPicker.pickAllSorted();
		if (null != picks)
		{
			for (PickInfo pickInfo : picks)
			{
				Node node = pickInfo.getNode();
				if (node.getParent() instanceof Wall3D)
				{
					ans = pickInfo;
					break;
				}
			}
		}
		return ans;
	}

	private PickInfo pickGround(java.awt.Point dropPoint)
	{
		PickInfo ans = null;
		roomPicker.setShapeLocation(dropPoint.x, dropPoint.y);
		PickInfo[] picks = roomPicker.pickAllSorted();
		if (null != picks)
		{
			for (PickInfo pickInfo : picks)
			{
				Node node = pickInfo.getNode();
				if ("Ground".equals(node.getUserData()))
				{
					ans = pickInfo;
					break;
				}
			}
		}
		return ans;
	}
	
	/**
	 * Pick the closest HomePieceOfFurniture to the given Point
	 */
	private HomePieceOfFurniture pickPieceOfFurnitureNearestMousePosition(
			java.awt.Point mousePosition)
	{
		return pickPieceOfFurnitureNearestMousePosition(mousePosition, false);
	}
	/**
	 * Pick the closest HomePieceOfFurniture to the given Point 
	 * conditionally excluding Banner objects
	 */
	private HomePieceOfFurniture pickPieceOfFurnitureNearestMousePosition(
			java.awt.Point mousePosition, boolean ignoreBanners)
	{
		try
		{
			HomePieceOfFurniture3D homePieceOfFurniture3D = 
				pickPieceOfFurniture3DNearestMousePosition(mousePosition, ignoreBanners);
			
			if(homePieceOfFurniture3D != null)
				return (HomePieceOfFurniture)homePieceOfFurniture3D.getUserData();
		}
		catch (Exception e)
		{
			logger.log(Level.SEVERE, "Error resolving nearest piece of furniture");
		}
		
		return null;
	}

	/**
	 * Pick the closest Node to the given Point
	 */
	@SuppressWarnings("cast")
	private HomePieceOfFurniture3D pickPieceOfFurniture3DNearestMousePosition(
			java.awt.Point mousePosition)
	{
		return pickPieceOfFurniture3DNearestMousePosition(mousePosition, false);
	}
	/**
	 * Pick the closest Node to the given Point
	 */
	@SuppressWarnings("cast")
	private HomePieceOfFurniture3D pickPieceOfFurniture3DNearestMousePosition(
			java.awt.Point mousePosition, boolean ignoreBanners)
	{
		if (roomPicker instanceof PickCanvas)
		{
			roomPicker.setShapeLocation(mousePosition.x, mousePosition.y);
			//PickInfo closest = roomPicker.pickClosest();
			PickInfo closestPieces[] = roomPicker.pickAllSorted();
			if (closestPieces != null)
			{
next: 			for (int i = 0; i < closestPieces.length; i++)
				{
					HomePieceOfFurniture3D piece = getHomePieceOfFurniture3D(closestPieces[i]);
					boolean isBanner = piece != null && piece.getUserData() instanceof BannerStringPieceOfFurniture;
					if (ignoreBanners && isBanner)
					{
						continue next;
					}
					if (piece != null)
					{
						/*
						 * Give precedence to a HomeFilePieceOfFurniture that's almost as close as a Banner
						 */
						for (int j = i + 1; isBanner && j < closestPieces.length; j++)
						{
							HomePieceOfFurniture3D next = getHomePieceOfFurniture3D(closestPieces[j]);
							if (next != null && !(next.getUserData() instanceof BannerStringPieceOfFurniture)
									&& (closestPieces[j].getClosestDistance() - closestPieces[i].getClosestDistance()) < 25)
							{
								return next;
							}
						}
						return (HomePieceOfFurniture3D)piece;
					}
				}
			}
		}
		else
		{
			logger.info("pickPieceOfFurniture3DNearestMousePosition() called when no pick canvas available");
		}
		return null;
	}
	private HomePieceOfFurniture3D getHomePieceOfFurniture3D(PickInfo pickInfo)
	{
		for (Node node = pickInfo.getNode().getParent(); node != null; node = node.getParent())
			if (node instanceof HomePieceOfFurniture3D)
				return (HomePieceOfFurniture3D)node;
		return null;
	}
	/**
	 * 
	 * Support for files as objects in SweetHome3D model
	 * 
	 * @see com.eteks.sweethome3d.swing.HomeComponent3D.HomePieceOfFurniture3D
	 * 
	 */
	private static class TexturablePieceOfFurniture3D extends HomePieceOfFurniture3D implements
			Texturable
	{
		private Home home;

		public TexturablePieceOfFurniture3D(HomePieceOfFurniture piece)
		{
			super(piece);
		}

		private void setHome(Home newHome)
		{
			home = newHome;
		}

		@Override
		public void applyTexture(Texture texture)
		{
			int texWidth = texture.getWidth();
			int texHeight = texture.getHeight();
			Object userData = getUserData();
			CustomFurnitureSupport textureSupport = null;
			if (userData instanceof BannerStringPieceOfFurniture)
			{
				textureSupport = BannerPieceFurnitureSupport.getInstance();
			}
			else if (userData instanceof HomeFilePieceOfFurniture)
			{
				textureSupport = HaiVanPassPictureFrameFurnitureSupport.getInstance();
			}
			else
			{
				logger.warning("node has no user data, cannot apply texture!");
				return;
			}
			HomePieceOfFurniture piece = (HomePieceOfFurniture)userData;
			try
			{
				Shape3D[] shapes = textureSupport.findShapesToTextureize(this);
				float texAspect = 1.0f * texWidth / texHeight;
				for (Shape3D shape3d : shapes)
				{
					Appearance app = shape3d.getAppearance();
					app.setTransparencyAttributes(transparencyAttributes);
					

					home.setPieceOfFurnitureSize(piece, piece.getHeight() * texAspect, piece
							.getDepth(), piece.getHeight());
					app.setTexture(texture);
				}
			}
			catch (CustomFurnitureSupportException e)
			{
				String msg = String.format("could not find shapes to textureize: file:[%s]", piece);
				logger.log(Level.WARNING, msg, e);
			}

		}

		/**
		 * Called indirectly by super class constructor
		 * 
		 * @see com.eteks.sweethome3d.swing.HomeComponent3D.HomePieceOfFurniture3D#setAppearanceCapabilities(javax.media.j3d.Appearance)
		 */
		@Override
		protected void setAppearanceCapabilities(Appearance appearance)
		{
			super.setAppearanceCapabilities(appearance);
			logger.finest("enabling texture writing capabilities for appearance");
			appearance.setCapability(Appearance.ALLOW_TEXGEN_WRITE);
			appearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
			appearance.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_WRITE);
		}
		
		@Override
		public String toString()
		{
			return String.format("%s@%x <%s>",getClass().getSimpleName(),hashCode(),getUserData());
		}
	}

	
	/*
	 *************************************************
	 * 		Piece selection handling methods
	 ************************************************* 
	 */
	
	private static final String FURNITURE_PIECE_SELECTION_GROUP_NAME = "_FURNITURE_PIECE_SELECTION_GROUP_NAME_";
	
	/**
	 *	Selection of the furniture piece under the cursor
	 *	a) If Ctrl button is down this piece will be added to the current selection
	 *	b) Otherwise the current selection will cleared and this will be the only piece selected
	 *
	 *  If the piece is already selected it's selection will not be changed
	 */
	
	private void selectFurniturePiece(MouseEvent mouseEvent)
	{
		Point position = mouseEvent.getPoint();
		
		HomePieceOfFurniture3D homePieceOfFurniture3D = pickPieceOfFurniture3DNearestMousePosition(position);
		
		if (homePieceOfFurniture3D == null)
			return;

		if (!mouseEvent.isControlDown())
			removeSelectionFromCurrentlySelectedPieces();
		if (mouseEvent.isControlDown() && isBranchGroupSelected(homePieceOfFurniture3D))
		{
			removeSelectionBranchGroup(homePieceOfFurniture3D);
		}
		else if (!isBranchGroupSelected(homePieceOfFurniture3D))
		{
			showFurniturePieceSelection(homePieceOfFurniture3D);
		}
	}	

	private BranchGroup getSelectionBranchGroupIfSelected(HomePieceOfFurniture3D homePieceOfFurniture3D)
	{
		return getSelectionBranchGroupIfSelected(homePieceOfFurniture3D, null);
	}
	
	@SuppressWarnings("unchecked")
	private BranchGroup getSelectionBranchGroupIfSelected(HomePieceOfFurniture3D homePieceOfFurniture3D, Transform3D boundTransform3D)
	{
		Node pieceSelectionTransformGroupNode = homePieceOfFurniture3D.getChild(0);
		
		if(pieceSelectionTransformGroupNode == null || (!(pieceSelectionTransformGroupNode instanceof TransformGroup)))
		{
			logger.warning("First child of the home piece of furniture is not a TransformGroup instance. Unable to apply the selection.");
			return null;
		}
		
		TransformGroup pieceSelectionTransformGroup = (TransformGroup)pieceSelectionTransformGroupNode;
		
		if(boundTransform3D != null)
			pieceSelectionTransformGroup.getTransform(boundTransform3D);
		
		Enumeration<Object> childrenEnum = pieceSelectionTransformGroup.getAllChildren();
		while(childrenEnum.hasMoreElements())
		{
			
			Object child = childrenEnum.nextElement();
			if(child instanceof BranchGroup)
			{
				String groupName = (String) ((BranchGroup)child).getUserData();
				if(groupName != null && groupName.equals(FURNITURE_PIECE_SELECTION_GROUP_NAME))
				{
					return ((BranchGroup)child);
				}
			}
		}
		return null;
	}
	
	private void addSelectionBranchGroup(HomePieceOfFurniture3D homePieceOfFurniture3D, BranchGroup selectionBranchGroup)
	{
		Node pieceSelectionTransformGroupNode = homePieceOfFurniture3D.getChild(0);
		
		if(pieceSelectionTransformGroupNode == null || (!(pieceSelectionTransformGroupNode instanceof TransformGroup)))
		{
			logger.warning("First child of the home piece of furniture is not a TransformGroup instance. Unable to apply the selection.");
			return;
		}
		
		TransformGroup pieceSelectionTransformGroup = (TransformGroup)pieceSelectionTransformGroupNode;
		pieceSelectionTransformGroup.addChild(selectionBranchGroup);
		pieceSelectionMap.put(homePieceOfFurniture3D, new Object());
	}
	

	private boolean isBranchGroupSelected(HomePieceOfFurniture3D homePieceOfFurniture3D)
	{
		Node pieceSelectionTransformGroupNode = homePieceOfFurniture3D.getChild(0);

		return pieceSelectionTransformGroupNode != null && (pieceSelectionTransformGroupNode instanceof TransformGroup)
				&& getSelectionBranchGroupIfSelected(homePieceOfFurniture3D) != null;
	}

	private void removeSelectionBranchGroup(HomePieceOfFurniture3D homePieceOfFurniture3D)
	{
		Node pieceSelectionTransformGroupNode = homePieceOfFurniture3D.getChild(0);
		
		if(pieceSelectionTransformGroupNode == null || (!(pieceSelectionTransformGroupNode instanceof TransformGroup)))
		{
			logger.warning("First child of the home piece of furniture is not a TransformGroup instance. Unable to apply the selection.");
			return;
		}
		
		BranchGroup selectionBranchGroup = getSelectionBranchGroupIfSelected(homePieceOfFurniture3D);
		if(selectionBranchGroup != null)
		{
			TransformGroup pieceSelectionTransformGroup = (TransformGroup)pieceSelectionTransformGroupNode;
			pieceSelectionTransformGroup.removeChild(selectionBranchGroup);
			selectionBranchGroup.removeAllChildren();
			pieceSelectionMap.remove(homePieceOfFurniture3D);
		}
	}
	
	private void showFurniturePieceSelection(HomePieceOfFurniture3D homePieceOfFurniture3D)
	{
		
		Transform3D boundTransform3D = new Transform3D();
		BranchGroup selectionBranchGroup = getSelectionBranchGroupIfSelected(homePieceOfFurniture3D, boundTransform3D);

		if(selectionBranchGroup != null)
			return;
		
		Appearance boundBoxAppearance = new Appearance();
		Material boundBoxMaterial = new Material();
		boundBoxMaterial.setCapability(Material.ALLOW_COMPONENT_WRITE);
		boundBoxMaterial.setDiffuseColor(new Color3f(LIGHT_BLUE));
		boundBoxAppearance.setCapability(Appearance.ALLOW_MATERIAL_WRITE);
		boundBoxAppearance.setMaterial(boundBoxMaterial);
		TransparencyAttributes boundBoxTransparencyAttributes = new TransparencyAttributes();
		boundBoxTransparencyAttributes.setTransparencyMode(TransparencyAttributes.SCREEN_DOOR);
//		boundBoxTransparencyAttributes.setDstBlendFunction(TransparencyAttributes.BLEND_SRC_ALPHA);			
		boundBoxTransparencyAttributes.setTransparency(0.5f);
		boundBoxAppearance.setTransparencyAttributes(boundBoxTransparencyAttributes);
		RenderingAttributes renderingAttributes = new RenderingAttributes();
		renderingAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
		boundBoxAppearance.setRenderingAttributes(renderingAttributes);
		PolygonAttributes polygonAttributes = new PolygonAttributes();
		polygonAttributes.setCapability(PolygonAttributes.ALLOW_CULL_FACE_WRITE);
		polygonAttributes.setCapability(PolygonAttributes.ALLOW_NORMAL_FLIP_WRITE);
		boundBoxAppearance.setPolygonAttributes(polygonAttributes);
		Box boundBoxNode = new Box((float)(0.6), (float)(0.6), (float)(0.6), boundBoxAppearance);			
		
		selectionBranchGroup = new BranchGroup();
		selectionBranchGroup.setUserData(FURNITURE_PIECE_SELECTION_GROUP_NAME);
		selectionBranchGroup.setCapability(BranchGroup.ALLOW_DETACH);
		
		selectionBranchGroup.addChild(boundBoxNode);
		addSelectionBranchGroup(homePieceOfFurniture3D, selectionBranchGroup);
	}
	
	private void removeSelectionFromCurrentlySelectedPieces()
	{
		Iterator<HomePieceOfFurniture3D> selectedPiecesIterator = pieceSelectionMap.keySet().iterator();
		
		while(selectedPiecesIterator.hasNext())
		{
			HomePieceOfFurniture3D selectedFurniturePiece = selectedPiecesIterator.next();
			removeSelectionBranchGroup(selectedFurniturePiece);
		}
		pieceSelectionMap.clear();
	}
	
	/*
	 *------------------------------------------------
	 *------------------------------------------------ 
	 */
	
	public void addGestureListener(final GestureListener listener)
	{
		synchronized(gestureEventListenerList)
		{
			gestureEventListenerList.add(GestureListener.class, listener);
		}
		logger.fine(String.format("added gesture listener [%s] to listener list [%s]",listener,gestureEventListenerList));
	}

	public void removeGestureListener(GestureListener listener)
	{
		synchronized(gestureEventListenerList)
		{
			gestureEventListenerList.remove(GestureListener.class, listener);
		}
		logger.fine(String.format("removed gesture listener [%s] from listener list [%s]",listener,gestureEventListenerList));
	}

	void fireGestureRecognizedEvent(GestureEvent gestureEvent)
	{
		Object[] gestureEventListeners = gestureEventListenerList.getListenerList();
		if(null == gestureEventListeners || gestureEventListeners.length < 1)
		{
			logger.warning("gesture event to fire, but no listeners registered to receive it");
		}
		for (int i = gestureEventListeners.length - 2; i >= 0; i -= 2)
		{
			if (gestureEventListeners[i] == GestureListener.class)
			{
				((GestureListener)gestureEventListeners[i + 1]).gestureRecognized(gestureEvent);
			}
		}
	}

	private FileThumbnailTextureLoader waitForfileThumbnailTextureLoader()
	{
		synchronized (fileThumbnailTextureLoaderLock)
		{
			if (null == fileThumbnailTextureLoader)
			{
				try
				{
					fileThumbnailTextureLoaderLock.wait();
				}
				catch (InterruptedException ie)
				{
					throw new RuntimeException(ie);
				}
			}
		}
		return fileThumbnailTextureLoader;
	}

	public class ImportGestureEventCallbackImpl implements ImportGestureEventCallback
	{
		private HomeFilePieceOfFurniture homeFilePieceOfFurniture;

		ImportGestureEventCallbackImpl(HomeFilePieceOfFurniture homeFilePiece)
		{
			homeFilePieceOfFurniture = homeFilePiece;
		}
		
		@Override
		public void importComplete(Object importedAs, Object toImport)
		{
			if (importedAs instanceof File[] && toImport instanceof File[])
			{
				File[] importedList = (File[])importedAs;
				File[] importTargetList = (File[])toImport;
				for (int i = 0; i < importedList.length; i++)
				{
					File imported = importedList[i];
					File importTarget = importTargetList[i];
					if (!homeFilePieceOfFurniture.getFile().equals(importTarget))
						throw new IllegalStateException("import target not equal to piece's file");
					homeFilePieceOfFurniture.setFile(imported);
					home.setPieceOfFurnitureName(homeFilePieceOfFurniture, imported.toString());
					FolderMonitor.getInstance().preload(imported);
					try
					{
						FilePieceOfFurnitureTextureManager.getInstance()
								.updateThumbnailFromNativeFile(importTarget, imported);
					}
					catch(Exception e)
					{
						logger.severe(e.getMessage());
					}
				}
				callbacks.remove(this);
				if(callbacks.isEmpty())
					FolderMonitor.getInstance().addListener(folderChangeListener);
			}
		}

		@Override
		public void importStarting(Object toImport)
		{
			
			File targetFile = toImport instanceof File ? (File)toImport : ((File[])toImport)[0];
			
			headsUpDisplay.getModel().setShowProgress(true);
			headsUpDisplay.getModel().setCurrentFile("Writing '" + targetFile.getName() + "'");
			headsUpDisplay.setProgress(0);
			
			if(callbacks.isEmpty())
				FolderMonitor.getInstance().removeListener(folderChangeListener);
			callbacks.add(this);
		}

		@Override
		public Status reportError(Object target, Error error, Object report)
		{
			if(transferCancelled.get())
			{
				transferCancelled.set(false);
				headsUpDisplay.getModel().setShowProgress(false);
				return Status.ABORT;
			}
			return Status.INDETERMINATE;
		}

		@Override
		public Status reportProgress(int totalPercents, int currentPercents, Object currentTarget)
		{
			headsUpDisplay.setProgress(currentPercents);
			if(transferCancelled.get())
			{
				transferCancelled.set(false);
				headsUpDisplay.getModel().setShowProgress(false);
				return Status.ABORT;
			}
			return Status.PROCEED;
		}

		@Override
		public Status reportWait(Object currentTarget)
		{
			File targetFile = currentTarget instanceof File ? (File)currentTarget : ((File[])currentTarget)[0];
			headsUpDisplay.getModel().setCurrentFile("Finalizing '" + targetFile.getName() + "'");
			if(transferCancelled.get())
			{
				transferCancelled.set(false);
				headsUpDisplay.getModel().setShowProgress(false);
				return Status.ABORT;
			}
			return Status.PROCEED;
		}

		@Override
		public Status reportCompleted(Object currentTarget)
		{
			headsUpDisplay.getModel().setShowProgress(false);
			if(transferCancelled.get())
			{
				transferCancelled.set(false);
				headsUpDisplay.getModel().setShowProgress(false);
				return Status.ABORT;
			}
			return Status.PROCEED;
		}

		@Override
		public void notifyBeforeCompleted(Object currentTarget, Object argument)
		{
		}
	}

	public ClipboardContentsViewer getClipboardContentsViewer()
	{
		return clipboardContentsViewer;
	}
}
